//
//  ActionScreenCaptureVC.swift
//  Hermes
//
//  Created by FH on 2021/6/25.
//  Copyright © 2021 Agora. All rights reserved.
//

import Foundation

fileprivate let SourcesListHeight: CGFloat = 370
fileprivate let CellWidth: Float = 190
fileprivate let CellHeight: Float = 160

class ActionScreenCaptureVC : FLViewController, PropsStatesContext {
    let appContext = HermesContext()
    private let localStates = StatesStore()
    // views
    private let contentSources = NSView()
    private let scrollView = NSScrollView()
    private let panelSettings = NSView()
    private let inputBitrate = FLInputField()
    private let selectFPS = NSPopUpButton()
    private let selectDimensions = NSPopUpButton()
    private let inputDimWidth = FLInputField()
    private let inputDimHeight = FLInputField()
    private let inputRectX = FLInputField()
    private let inputRectY = FLInputField()
    private let inputRectWidth = FLInputField()
    private let inputRectHeight = FLInputField()
    private let chkIncludeScreen = FLCheckbox()
    private let inputThumbWidth = FLInputField()
    private let inputThumbHeight = FLInputField()
    private let inputIconWidth = FLInputField()
    private let inputIconHeight = FLInputField()
    private let btnUpdateSettings = NSButton()
    // inner states
    private var hasLoadCaptureSources = false
    private var updateCaptureSources = false
    @PropState
    var includeScreenShots = true
    @PropState
    var thumbScaleSize = CGSize(width: 480, height: 360)
    @PropState
    var iconScaleSize = CGSize(width: 32, height: 32)
    @PropState
    var captureFreq = 5
    @PropState
    var bitrate = 2000
    @PropState
    var dimensions = AppVideoDimension.custom(size: CGSize.zero)
    @PropState
    var rectangle = CGRect.zero
    
    override func setWindowStyle(currWindow: NSWindow) {
        currWindow.styleMask.remove(.fullSizeContentView)
        currWindow.styleMask.remove(.resizable)
        currWindow.title = self.appContext.mainStates.captureSharing ? "Update Params Of Screen Capture" : "Start Screen Capture"
        currWindow.isMovableByWindowBackground = true
        let size = self.view.bounds.size
        currWindow.setContentSize(CGSize(width: size.width, height: size.height))
        currWindow.center()
        self.dataBinding()
    }
    
    override func setViewStyle() {
        let height: CGFloat = self.appContext.mainStates.captureSharing ? (520 - SourcesListHeight) : 520
        self.view.frame = CGRect(x: 0, y: 0, width: 800, height: height)
        // init @PropState
        self.bindStates()
    }
    
    private func dataBinding() {
        if !self.hasLoadCaptureSources {
            self.hasLoadCaptureSources = true
            // observer enum capture source
            self.appContext.mainStates.$manualEnumCaptureSourcesTrigger.signal()
                .filter { $0 }
                .subscribeNext {[weak self] _ in
                if let self = self, !self.appContext.mainStates.captureSharing {
                    let dataSource = getCaptureSources(showScreenShots: self.includeScreenShots,
                                                       thumbScaleSize: self.thumbScaleSize,
                                                       iconScaleSize: self.iconScaleSize)
                    self.layoutSourceList(dataSource)
                }
            }
            // TODO: remove，scriptEnumCaptureSourcesTrigger的autoReset
            self.appContext.mainStates.$scriptEnumCaptureSourcesTrigger.signal()
                .ignoreNilValue()
                .subscribeNext {[weak self] sources in
                    if let self = self, !self.appContext.mainStates.captureSharing {
                        let dataSource = getCaptureSources(by: sources!)
                        self.layoutSourceList(dataSource)
                    }
                }
            // bind context's states to control
            self.$thumbScaleSize.signal().subscribeNext {[weak self] val in
                if let self = self {
                    self.inputThumbWidth.flText = "\(val.width)"
                    self.inputThumbHeight.flText = "\(val.height)"
                }
            }
            self.$iconScaleSize.signal().subscribeNext {[weak self] val in
                if let self = self {
                    self.inputIconWidth.flText = "\(val.width)"
                    self.inputIconHeight.flText = "\(val.height)"
                }
            }
            self.$includeScreenShots.bindTo(self.chkIncludeScreen)
            let fps = Array(1...15).map { "\($0)" }
            self.selectFPS.addItems(withTitles: fps)
            self.$captureFreq.bindTo(self.selectFPS)
            self.$bitrate.bindTo(self.inputBitrate)
            self.selectDimensions.addItems(withTitles: AppVideoDimension.allCaseDescriptions)
            self.$dimensions.bindTo(self.selectDimensions) {[weak self] (_, val) in
                if let self = self {
                    self.updateDimensions(value: val)
                }
            }
            self.$rectangle.signal().subscribeNext {[weak self] val in
                if let self = self {
                    self.inputRectX.flText = "\(val.origin.x)"
                    self.inputRectY.flText = "\(val.origin.y)"
                    self.inputRectWidth.flText = "\(val.size.width)"
                    self.inputRectHeight.flText = "\(val.size.height)"
                }
            }
            // subscribe control's data to context
            self.selectDimensions.rac_eventTouch.subscribeNext {[weak self] select in
                if let self = self {
                    let item = AppVideoDimension.allCaseItems[self.selectDimensions.indexOfSelectedItem]
                    self.updateDimensions(value: item)
                }
            }
            self.btnUpdateSettings.rac_eventTouch.subscribeNext {[weak self] _ in
                if let self = self {
                    // update capture settings
                    if let fps = Int(self.selectFPS.itemTitles[self.selectFPS.indexOfSelectedItem]), fps != self.captureFreq {
                        self.$captureFreq.update(value: fps, self.localStates)
                    }
                    var dimValue = AppVideoDimension.allCaseItems[self.selectDimensions.indexOfSelectedItem]
                    if case .custom = dimValue {
                        if let width = Float(self.inputDimWidth.flText.trimWhiteSpaces()),
                           let height = Float(self.inputDimHeight.flText.trimWhiteSpaces()) {
                            dimValue = AppVideoDimension.custom(size: CGSize(width: width, height: height))
                        } else {
                            FLShowCloseAlert("input value of DimensionWidth or DimensionHeight are not available.")
                            self.btnUpdateSettings.flTitle = self.btnUpdateSettings.flTitle.switchButtonTitle()
                            return
                        }
                    }
                    if dimValue != self.dimensions {
                        self.$dimensions.update(value: dimValue, self.localStates)
                    }
                    if let value = Int(self.inputBitrate.flText.trimWhiteSpaces()) {
                        if value != self.bitrate {
                            self.$bitrate.update(value: value, self.localStates)
                        }
                    } else {
                        FLShowCloseAlert("input value of BitRate is not available.")
                        self.btnUpdateSettings.flTitle = self.btnUpdateSettings.flTitle.switchButtonTitle()
                        return
                    }
                    if let x = Float(self.inputRectX.flText.trimWhiteSpaces()),
                       let y = Float(self.inputRectY.flText.trimWhiteSpaces()),
                       let width = Float(self.inputRectWidth.flText.trimWhiteSpaces()),
                       let height = Float(self.inputRectHeight.flText.trimWhiteSpaces()) {
                        let rect = CGRect(x: x, y: y, width: width, height: height)
                        if rect != self.rectangle {
                            self.$rectangle.update(value: rect, self.localStates)
                        }
                    } else {
                        FLShowCloseAlert("input value of RectX、RectY、RectWidth or RectHeight are not available.")
                        self.btnUpdateSettings.flTitle = self.btnUpdateSettings.flTitle.switchButtonTitle()
                        return
                    }
                    // trigger updateParams
                    if self.appContext.mainStates.captureSharing {
                        let params = AgoraScreenCaptureParameters()
                        params.bitrate = self.bitrate;
                        params.frameRate = self.captureFreq
                        params.dimensions = self.dimensions.rawValue
                        self.appContext.scriptsInvoker.callCustomAction(ActionNameUpdateCaptureParams,
                                                                        instrContext: [
                                                                            "params": params
                                                                        ]) {[weak self] in
                            self?.dismiss(nil)
                        }
                    } else {
                        // update enum capture sources
                        var needUpdate = false
                        if let thumbWidth = Float(self.inputThumbWidth.flText.trimWhiteSpaces()),
                           let thumbHeight = Float(self.inputThumbHeight.flText.trimWhiteSpaces()),
                           thumbWidth > 0 && thumbHeight > 0 {
                            let thumbSize = CGSize(width: thumbWidth, height: thumbHeight)
                            if self.thumbScaleSize != thumbSize {
                                self.$thumbScaleSize.update(value: thumbSize, self.localStates)
                                needUpdate = true
                            }
                        } else {
                            FLShowCloseAlert("input value of ThumbWidth or ThumbHeight are not available.")
                            self.btnUpdateSettings.flTitle = self.btnUpdateSettings.flTitle.switchButtonTitle()
                            return
                        }
                        if let iconWidth = Float(self.inputIconWidth.flText.trimWhiteSpaces()),
                           let iconHeight = Float(self.inputIconHeight.flText.trimWhiteSpaces()),
                           iconWidth > 0 && iconHeight > 0 {
                            let iconSize = CGSize(width: iconWidth, height: iconHeight)
                            if self.iconScaleSize != iconSize {
                                self.$iconScaleSize.update(value: iconSize, self.localStates)
                                needUpdate = true
                            }
                        } else {
                            FLShowCloseAlert("input value of IconWidth or IconHeight are not available.")
                            self.btnUpdateSettings.flTitle = self.btnUpdateSettings.flTitle.switchButtonTitle()
                            return
                        }
                        let includeScreen = self.chkIncludeScreen.state == .on
                        if self.includeScreenShots != includeScreen {
                            self.$includeScreenShots.update(value: includeScreen, self.localStates)
                            needUpdate = true
                        }
                        if needUpdate {
                            self.updateCaptureSources = true
                            self.triggerEnumCaptureSources()
                        }
                    }
                }
            }
            if !self.appContext.mainStates.captureSharing {
                self.triggerEnumCaptureSources()
            }
        }
    }
    
    private func updateDimensions(value: AppVideoDimension) {
        let index = value.getCurrIndex()
        if index != self.selectDimensions.indexOfSelectedItem {
            self.selectDimensions.selectItem(at: index)
        }
        if case .custom(let size) = value {
            self.inputDimWidth.flText = "\(size.width)"
            self.inputDimHeight.flText = "\(size.height)"
            self.inputDimWidth.isEnabled = true
            self.inputDimHeight.isEnabled = true
        } else {
            self.inputDimWidth.flText = "\(value.rawValue.width)"
            self.inputDimHeight.flText = "\(value.rawValue.height)"
            self.inputDimWidth.isEnabled = false
            self.inputDimHeight.isEnabled = false
        }
    }

    private func triggerEnumCaptureSources() {
        self.appContext.scriptsInvoker.callCustomAction(ActionNameEnumCaptureSources,
                                                        instrContext: ["thumbSize": self.thumbScaleSize,
                                                                       "iconSize": self.iconScaleSize,
                                                                       "includeScreen": self.includeScreenShots])
    }

    private func layoutSourceList(_ dataSource: [CaptureSourceInfo]) {
        if self.updateCaptureSources {
            self.updateCaptureSources = false
            for item in self.contentSources.subviews {
                if let view = item as? CaptureSourceView {
                    yoga.removeNode(by: view)
                }
            }
        }
        if let node = self.yoga.getNode(by: self.contentSources) {
            node.minHeight = SourcesListHeight.toFLValue()
            node.height = (CellHeight * ceilf((Float(dataSource.count) / 4))).toFLValue()
            var pos = 0
            for item in dataSource {
                let cell = CaptureSourceView()
                cell.bindLayout(to: yoga, self.contentSources) { it in
                    it.width = CellWidth.toFLValue()
                    it.height = CellHeight.toFLValue()
                    if pos > 0 {
                        it.marginLeft = 8
                    }
                }
                cell.type = item.type
                cell.sourceId = item.sourceId
                cell.lblTitle.flText = item.sourceName
                cell.imgThumb.image = item.thumbImage.image
                if let icon = item.iconImage?.image {
                    cell.imgIcon.image = icon
                }
                cell.rac_observerEvent(.leftMouseDoubleDown).subscribeNext {[weak self] (cell, _) in
                    if let self = self, !self.appContext.mainStates.captureSharing, let cell = cell as? CaptureSourceView {
                        let forWindow = cell.type == .window
                        let params = AgoraScreenCaptureParameters()
                        params.bitrate = self.bitrate;
                        params.frameRate = self.captureFreq
                        params.dimensions = self.dimensions.rawValue
                        self.appContext.scriptsInvoker.callCustomAction(ActionNameStartCaptureSharing,
                                                                        instrContext: [
                                                                            "forWindow": forWindow,
                                                                            "sourceId": cell.sourceId!,
                                                                            "rect": self.rectangle,
                                                                            "params": params
                                                                        ])
                        self.dismiss(nil)
                    }
                }
                pos += 1
                if pos == 4 {
                    pos = 0
                }
            }
            yoga.apply()
        }
    }
    
    override func layoutUI(needsLayout: Bool, args: [String : Any?]? = nil) {
        self.view.flBackgroundColor = .darkGray
        // sources list
        self.scrollView.bind(to: yoga, self.view) { it in
            it.display = self.appContext.mainStates.captureSharing ? .none : .flex
            it.marginHorizontal = 8
            it.marginVertical = 4
            it.height = SourcesListHeight.toFLValue()
        }
        // placeholder
        self.contentSources.bind(to: yoga, self.scrollView) { it in
            it.display = self.appContext.mainStates.captureSharing ? .none : .flex
            it.direction = .row
            it.wrap = .wrap
        }
        self.contentSources.flBackgroundColor = .darkGray
        self.scrollView.contentView.documentView = self.contentSources
        // settings
        self.panelSettings.bind(to: yoga, self.view) { it in
            it.marginHorizontal = 8
            it.marginBottom = 4
            it.marginTop = 24
            it.flexGrow = 1
        }
        // line-1
        let div1 = NSView()
        div1.bind(to: yoga, self.panelSettings) { it in
            it.direction = .row
            it.height = 36
            it.alignItems = .center
        }
        let lblThumbWidth = FLLabel()
        lblThumbWidth.bind(to: yoga, div1) { it in
            it.height = 20
        }
        lblThumbWidth.flText = "ThumbWidth: "
        lblThumbWidth.textColor = .white
        lblThumbWidth.font = NSFont.systemFont(ofSize: 12)
        self.inputThumbWidth.bind(to: yoga, div1) { it in
            it.width = 60
            it.height = 24
        }
        self.inputThumbWidth.isEnabled = !self.appContext.mainStates.captureSharing
        let lblThumbHeight = FLLabel()
        lblThumbHeight.bind(to: yoga, div1) { it in
            it.height = 20
            it.marginLeft = 4
        }
        lblThumbHeight.flText = "ThumbHeight: "
        lblThumbHeight.textColor = .white
        lblThumbHeight.font = NSFont.systemFont(ofSize: 12)
        self.inputThumbHeight.bind(to: yoga, div1) { it in
            it.width = 60
            it.height = 24
        }
        self.inputThumbHeight.isEnabled = !self.appContext.mainStates.captureSharing
        let lblIconWidth = FLLabel()
        lblIconWidth.bind(to: yoga, div1) { it in
            it.height = 20
            it.marginLeft = 4
        }
        lblIconWidth.flText = "IconWidth: "
        lblIconWidth.textColor = .white
        lblIconWidth.font = NSFont.systemFont(ofSize: 12)
        self.inputIconWidth.bind(to: yoga, div1) { it in
            it.width = 60
            it.height = 24
        }
        self.inputIconWidth.isEnabled = !self.appContext.mainStates.captureSharing
        let lblIconHeight = FLLabel()
        lblIconHeight.bind(to: yoga, div1) { it in
            it.height = 20
            it.marginLeft = 4
        }
        lblIconHeight.flText = "IconHeight: "
        lblIconHeight.textColor = .white
        lblIconHeight.font = NSFont.systemFont(ofSize: 12)
        self.inputIconHeight.bind(to: yoga, div1) { it in
            it.width = 60
            it.height = 24
        }
        self.inputIconHeight.isEnabled = !self.appContext.mainStates.captureSharing
        self.chkIncludeScreen.bind(to: yoga, div1) { it in
            it.height = 24
            it.marginLeft = 12
        }
        self.chkIncludeScreen.flTitleColor = .white
        self.chkIncludeScreen.font = NSFont.systemFont(ofSize: 12)
        self.chkIncludeScreen.flTitle = "Include Screen Shots"
        self.chkIncludeScreen.isEnabled = !self.appContext.mainStates.captureSharing
        // line-2
        let div2 = NSView()
        div2.bind(to: yoga, self.panelSettings) { it in
            it.direction = .row
            it.height = 36
            it.alignItems = .center
        }
        let lblFPS = FLLabel()
        lblFPS.bind(to: yoga, div2) { it in
            it.height = 20
        }
        lblFPS.flText = "CaptureFreq: "
        lblFPS.textColor = .white
        lblFPS.font = NSFont.systemFont(ofSize: 12)
        self.selectFPS.bind(to: yoga, div2) { it in
            it.width = 80
            it.height = 30
        }
        let lblBitrate = FLLabel()
        lblBitrate.bind(to: yoga, div2) { it in
            it.height = 20
            it.marginLeft = 4
        }
        lblBitrate.flText = "BitRate: "
        lblBitrate.textColor = .white
        lblBitrate.font = NSFont.systemFont(ofSize: 12)
        self.inputBitrate.bind(to: yoga, div2) { it in
            it.width = 100
            it.height = 24
        }
        let lblDimensions = FLLabel()
        lblDimensions.bind(to: yoga, div2) { it in
            it.height = 20
            it.marginLeft = 4
        }
        lblDimensions.flText = "Dimensions: "
        lblDimensions.textColor = .white
        lblDimensions.font = NSFont.systemFont(ofSize: 12)
        self.selectDimensions.bind(to: yoga, div2) { it in
            it.width = 140
            it.height = 30
        }
        let lblDimWidth = FLLabel()
        lblDimWidth.bind(to: yoga, div2) { it in
            it.height = 20
        }
        lblDimWidth.flText = "Width: "
        lblDimWidth.textColor = .white
        lblDimWidth.font = NSFont.systemFont(ofSize: 12)
        self.inputDimWidth.bind(to: yoga, div2) { it in
            it.width = 50
            it.height = 24
        }
        let lblDimHeight = FLLabel()
        lblDimHeight.bind(to: yoga, div2) { it in
            it.height = 20
            it.marginLeft = 2
        }
        lblDimHeight.flText = "Height: "
        lblDimHeight.textColor = .white
        lblDimHeight.font = NSFont.systemFont(ofSize: 12)
        self.inputDimHeight.bind(to: yoga, div2) { it in
            it.width = 50
            it.height = 24
        }
        // line-3
        let div3 = NSView()
        div3.bind(to: yoga, self.panelSettings) { it in
            it.direction = .row
            it.height = 36
            it.alignItems = .center
        }
        let lblRectX = FLLabel()
        lblRectX.bind(to: yoga, div3) { it in
            it.height = 20
        }
        lblRectX.flText = "RectX: "
        lblRectX.textColor = .white
        lblRectX.font = NSFont.systemFont(ofSize: 12)
        self.inputRectX.bind(to: yoga, div3) { it in
            it.width = 50
            it.height = 24
        }
        let lblRectY = FLLabel()
        lblRectY.bind(to: yoga, div3) { it in
            it.height = 20
            it.marginLeft = 4
        }
        lblRectY.flText = "RectY: "
        lblRectY.textColor = .white
        lblRectY.font = NSFont.systemFont(ofSize: 12)
        self.inputRectY.bind(to: yoga, div3) { it in
            it.width = 50
            it.height = 24
        }
        let lblRectWidth = FLLabel()
        lblRectWidth.bind(to: yoga, div3) { it in
            it.height = 20
            it.marginLeft = 4
        }
        lblRectWidth.flText = "RectWidth: "
        lblRectWidth.textColor = .white
        lblRectWidth.font = NSFont.systemFont(ofSize: 12)
        self.inputRectWidth.bind(to: yoga, div3) { it in
            it.width = 50
            it.height = 24
        }
        let lblRectHeight = FLLabel()
        lblRectHeight.bind(to: yoga, div3) { it in
            it.height = 20
            it.marginLeft = 4
        }
        lblRectHeight.flText = "RectHeight: "
        lblRectHeight.textColor = .white
        lblRectHeight.font = NSFont.systemFont(ofSize: 12)
        self.inputRectHeight.bind(to: yoga, div3) { it in
            it.width = 50
            it.height = 24
        }
        self.btnUpdateSettings.bind(to: yoga, div3) { it in
            it.position = .absolute
            it.height = 26
            it.width = 100
            it.right = 8
        }
        self.btnUpdateSettings.flCornerRadius = 4
        self.btnUpdateSettings.flTitleColor = .darkGray
        self.btnUpdateSettings.flBackgroundColor = .white
        self.btnUpdateSettings.flTitle = self.appContext.mainStates.captureSharing ? "UpdateParams" : "SaveSettings"
        yoga.apply()
    }
}

// REMARK: cell view

fileprivate class CaptureSourceView : FLCommonView {
    let bgView = NSView()
    let imgThumb = NSImageView()
    let imgIcon = NSImageView()
    let lblTitle = FLLabel()
    var type: CaptureSourceType = .unknown
    var sourceId: CGWindowID? = nil
    
    override func layoutUI(yoga: FLContext) {
        let lblHeight: Float = 20
        self.bgView.bind(to: yoga, self) { it in
            it.flexGrow = 1
        }
        
        self.imgThumb.bind(to: yoga, self.bgView) { it in
            it.width = CellWidth.toFLValue()
            it.height = (CellHeight - lblHeight).toFLValue()
        }
        
        self.lblTitle.bind(to: yoga, self.bgView) { it in
            it.width = CellWidth.toFLValue()
            it.height = lblHeight.toFLValue()
        }
        self.lblTitle.textColor = .white
        self.lblTitle.alignment = .center
        self.lblTitle.font = NSFont.systemFont(ofSize: 12)
        
        self.imgIcon.bind(to: yoga, self.bgView) { it in
            it.position = .absolute
            it.width = 32
            it.height = 32
            it.left = 8
            it.bottom = (lblHeight - 4).toFLValue()
        }
    }
}

// REMARK: help methods & struct

fileprivate enum CaptureSourceType {
    case unknown
    case window
    case screen
    case custom
}

fileprivate class ThumbImageBuffer {
    var image: NSImage! = nil
    var length: UInt = 0
    var height: UInt = 0
    var width: UInt = 0
}

fileprivate class CaptureSourceInfo {
    var type: CaptureSourceType = .unknown
    var sourceId: CGWindowID = 0
    var sourceName = ""
    var thumbImage: ThumbImageBuffer! = nil
    var iconImage: ThumbImageBuffer? = nil
    var processPath = ""
    var processTitle = ""
    var primaryMonitor = false
    var indexOfMonitor = 0
}

extension CaptureSourceInfo : CustomDebugStringConvertible {
    var debugDescription: String {
        return "[\(self.type == .window ? "window" : "screen")] \(self.sourceId) -> \(self.sourceName)"
    }
}

// TODO: 需要看一下实际返回值的类型
fileprivate func getCaptureSources(by scriptInstrResult: Any) -> [CaptureSourceInfo] {
    var notResponds = false
    if let obj = scriptInstrResult as? NSObject {
        if let sourceId = getObjValueByKeyPath("sourceId", obj) as? CGWindowID {
            
        } else {
            notResponds = true
        }
    }
    return []
}

fileprivate func getCaptureSources(showScreenShots: Bool, thumbScaleSize: CGSize, iconScaleSize: CGSize) -> [CaptureSourceInfo] {
    var list = [CaptureSourceInfo]()
    // get screen list
    let screens = NSScreen.screens.sorted { (item1, item2) in
        return item1.frame.origin.x < item2.frame.origin.x
    }
    if showScreenShots {
        var index = 1
        for screen in screens {
            if let screenId = screen.deviceDescription[NSDeviceDescriptionKey(rawValue: "NSScreenNumber")] as? CGDirectDisplayID {
                let info = CaptureSourceInfo()
                info.type = .screen
                info.sourceId = screenId
                info.primaryMonitor = screen.frame.origin.x == 0
                info.indexOfMonitor = index
                info.sourceName = info.primaryMonitor ? "主屏幕" : "屏幕-\(info.indexOfMonitor)"
                let imageInfo = ThumbImageBuffer()
                let image = getScreenThumbImage(screenId)
                let resize = handleResize(thumbScaleSize, image.size)
                imageInfo.image = image.resized(to: resize) ?? image
                imageInfo.width = UInt(resize.width)
                imageInfo.height = UInt(resize.height)
                info.thumbImage = imageInfo
                list.append(info)
                index += 1
            }
        }
    }
    // get window list
    if let windowDicCFArray = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], 0) {
        let windowDicList = windowDicCFArray as NSArray
        for windowElement in windowDicList {
            let windowDic = windowElement
            if let windowDic = windowDic as? NSDictionary {
                // filter window
                guard let alpha = windowDic[kCGWindowAlpha] as? Int32, alpha > 0 else {
                    continue
                }
                let windowBounds = windowDic[NSString(string: kCGWindowBounds)] as! CFDictionary
                guard let pid = windowDic[kCGWindowOwnerPID] as? Int else {
                    continue
                }
                guard let bounds = CGRect(dictionaryRepresentation: windowBounds), bounds.width > 96 && bounds.height > 96 else {
                    continue
                }
                var name: String?
                if let ownerName = windowDic[NSString(string: kCGWindowOwnerName)] {
                    let cfName = ownerName as! CFString
                    name = NSString(string: cfName) as String
                }
                if let detailName = windowDic[kCGWindowName] as? String, let name = name,
                   // TODO: remove, tmp to hide Hermes app
                   (name == "Dock" || detailName == "Dock" || (name == "Hermes" && detailName.contains("Hermes Mac"))) {
                    continue
                }
                let idNumber = windowDic[NSString(string: kCGWindowNumber)] as! CFNumber
                let id: CGWindowID = (idNumber as NSNumber).uint32Value
                guard let image = getWindowThumbImage(id) else {
                    continue
                }
                // ignore sub windows on Hermes App
                if name == "Hermes" && bounds.origin.x != 0 {
                    continue
                }
                
                let info = CaptureSourceInfo()
                info.type = .window
                info.sourceId = id
                info.sourceName = name ?? "Unknown"
                let imageInfo = ThumbImageBuffer()
                let resize = handleResize(thumbScaleSize, image.size)
                imageInfo.image = image.resized(to: resize) ?? image
                imageInfo.width = UInt(resize.width)
                imageInfo.height = UInt(resize.height)
                info.thumbImage = imageInfo
                // icon of app
                if let app = NSRunningApplication(processIdentifier: pid_t(pid)),
                   let icon = app.icon, icon.representations.count > 0 {
                    let iconSize = handleResize(iconScaleSize, icon.size)
                    let resizeIcon = icon.resized(to: iconSize)!
                    let iconInfo = ThumbImageBuffer()
                    iconInfo.image = resizeIcon
                    iconInfo.width = UInt(iconSize.width)
                    iconInfo.height = UInt(iconSize.height)
                    info.iconImage = iconInfo
                }
                list.append(info)
            }
        }
    }
    return list
}

fileprivate func getWindowThumbImage(_ windowId: CGWindowID) -> NSImage? {
    if let screenShot = CGWindowListCreateImage(CGRect.null, .optionIncludingWindow, CGWindowID(windowId), CGWindowImageOption.boundsIgnoreFraming) {
        let bitmapRep = NSBitmapImageRep(cgImage: screenShot)
        let image = NSImage()
        image.addRepresentation(bitmapRep)
        return image
    } else {
        return nil
    }
}

fileprivate func getScreenThumbImage(_ screenId: CGDirectDisplayID) -> NSImage {
    if let screenShot = CGDisplayCreateImage(screenId) {
        let bitmapRep = NSBitmapImageRep(cgImage: screenShot)
        let image = NSImage()
        image.addRepresentation(bitmapRep)
        return image
    } else {
        return NSImage()
    }
}

fileprivate func handleResize(_ targetSize: CGSize, _ sourceSize: CGSize) -> CGSize {
    var originSize = targetSize
    var widthScale = targetSize.width > 0 ? (sourceSize.width / targetSize.width) : 1
    if targetSize.width <= 0 || targetSize.width > sourceSize.width {
        originSize.width = sourceSize.width
        widthScale = 1
    }
    
    var heightScale = targetSize.height > 0 ? (sourceSize.height / targetSize.height) : 1
    if targetSize.height <= 0 || targetSize.height > sourceSize.height {
        originSize.height = sourceSize.height
        heightScale = 1
    }
    
    var expectedWidth = sourceSize.width / heightScale
    var expectedHeight = sourceSize.height / widthScale
    let size1 = targetSize.width * expectedHeight
    let size2 = targetSize.height * expectedWidth
    if size1 <= size2 {
        expectedWidth = originSize.width
    } else {
        expectedHeight = originSize.height
    }
    return CGSize(width: expectedWidth, height: expectedHeight)
}
